<?php defined( 'BASEPATH' ) or die( 'Restricted' );

/* This code is Copyright (C) by TMLA INC  ALL RIGHTS RESERVED. */

// Helper class for record assembly, probably belongs somewhere else, but I don't know where to put it.
// eventually this could accept formatting instructions, and could be a public class to be used
// by all the ach models.  For now we'll just have it here and see how it works
class arbItemFactory
{
	static public function padLeft( $padChar = ' ' )
	{
		$ob = new arbItemPadLeft();
		return $ob->padChar( $padChar );
	}
	//------------------------------------------------------------------------
	static public function padRight( $padChar = ' ' )
	{
		$ob = new arbItemPadRight();
		return $ob->padChar ( $padChar );
	}
}
//------------------------------------------------------------------------
abstract class arbItem extends tgsfBase
{
	protected $_ro_paddingType;
	protected $_ro_length;
	protected $_ro_fieldName;
	protected $_ro_appendValue;
	protected $_ro_prependValue;
	protected $_ro_padChar;
	protected $_ro_enforceNumeric = false;
	protected $_ro_enforceLength = false;
	protected $_ro_staticValue = null;
	protected $_ro_formatCurrency = false;
	protected $_ro_concatFields = null;
	protected $_ro_concatDelimiter = '';
	protected $_ro_truncateRight;
	protected $_ro_truncateLeft;

	protected $recordBuilder;

	//------------------------------------------------------------------------
	/**
	* Sets a static value for this item.  If a static value is set no value will be used
	* from the datasource.  If your code calls this then later decides to
	* use the ds, simply call this method and pass NULL
	* @param String The value.  To remove a static value pass NULL
	*/
	public function &staticValue( $value )
	{
		$this->_ro_staticValue = $value;
		return $this;
	}
	public function getStaticValue()
	{
		return $this->_ro_staticValue;
	}
	//------------------------------------------------------------------------
	/**
	* Sets the padding character
	* @param String The character to use when padding
	*/
	public function &padChar( $padChar )
	{
		$this->_ro_padChar = $padChar;
		return $this;
	}
	//------------------------------------------------------------------------
	/**
	* Sets the field name that is used when getting values from the datasource.
	*/
	public function &fieldName( $value )
	{
		$this->_ro_fieldName = $value;
		return $this;
	}
	//------------------------------------------------------------------------
	/**
	* Sets the source field names used when getting values from the datasource.
	* Expected to be set in conjunction with fieldName, and fieldName will then
	* be used solely for identification and in record parsing
	*/
	public function &concatFields( $fieldNames, $delimiter = '' )
	{
		$this->_ro_concatFields = $fieldNames;
		$this->_ro_concatDelimiter = $delimiter;
		return $this;
	}
	//------------------------------------------------------------------------
	/**
	* Gets the field name used when getting values from the datasource.
	*/
	public function getFieldName()
	{
		return $this->_ro_fieldName;
	}
	//------------------------------------------------------------------------
	/**
	* Gets the concat fields used when getting values from the datasource.
	*/
	public function getConcatFields()
	{
		return $this->_ro_concatFields;
	}
	//------------------------------------------------------------------------
	/**
	* Gets the concat delimiter
	*/
	public function getConcatDelimiter()
	{
		return $this->_ro_concatDelimiter;
	}
	//------------------------------------------------------------------------
	/**
	* Truncates the field value to the given length, discarding the rightmost characters
	*/
	public function &truncateRight( $value )
	{
		$this->_ro_truncateRight = $value;
		return $this;
	}
	//------------------------------------------------------------------------
	/**
	* Truncates the field value to the given length, discarding the leftmost characters
	*/
	public function &truncateLeft( $value )
	{
		$this->_ro_truncateLeft = $value;
		return $this;
	}
	//------------------------------------------------------------------------
	/**
	* Gets the truncate right length
	*/
	public function getTruncateRight()
	{
		return $this->_ro_truncateRight;
	}
	//------------------------------------------------------------------------
	/**
	* Gets the truncate left length
	*/
	public function getTruncateLeft()
	{
		return $this->_ro_truncateLeft;
	}
	//------------------------------------------------------------------------
	/**
	* Prepends the field value with the given text
	*/
	public function &prependValue( $value )
	{
		$this->_ro_prependValue = $value;
		return $this;
	}
	//------------------------------------------------------------------------
	/**
	* Gets the prepend value
	*/
	public function getPrependValue()
	{
		return $this->_ro_prependValue;
	}
	//------------------------------------------------------------------------
	/**
	* Appends the field value with the given text
	*/
	public function &appendValue( $value )
	{
		$this->_ro_appendValue = $value;
		return $this;
	}
	//------------------------------------------------------------------------
	/**
	* Gets the append value
	*/
	public function getAppendValue()
	{
		return $this->_ro_appendValue;
	}
	//------------------------------------------------------------------------
	/**
	* Sets the length of the field in the record
	* If you need to enforce this length to be exact, use the ->enforceLength method
	* @param Int the length of the field to generate
	*/
	public function &length( $length )
	{
		$this->_ro_length = $length;
		return $this;
	}
	public function getLength()
	{
		return $this->_ro_length;
	}
	//------------------------------------------------------------------------
	/**
	* Do we enforce a requirement of the data being numeric
	* causes an exception to be thrown upon execution not
	* @param Bool T/F - enforce?
	*/
	public function &enforceNumeric( $value = true )
	{
		$this->_ro_enforceNumeric = $value;
		return $this;
	}
	//------------------------------------------------------------------------
	/**
	* Do we enforce the length
	* causes an exception to be thrown upon execution if the length of the
	* data does not match the length specified
	* @param Bool T/F - enforce?
	*/
	public function &enforceLength( $value = true )
	{
		$this->_ro_enforceLength = $value;
		return $this;
	}
	//------------------------------------------------------------------------
	/**
	* Causes the _getData method to return a properly formatted currency value
	* @param Bool T/F - format as currency?
	*/
	public function &formatCurrency( $value = true )
	{
		$this->_ro_formatCurrency = $value;
		return $this;
	}
	//------------------------------------------------------------------------
	/**
	* Internal method for getting the data to work with.
	* @param DataSource: A datasource containing the field to work with for this record
	*/
	protected function _getData( $ds )
	{
		if ( $this->_ro_staticValue === null )
		{
		
			$concatFields = $this->_ro_concatFields;

			if ( ! is_array( $concatFields ) )
			{
				$concatFields = array( $this->_ro_fieldName );
			}
			
			$values = array();

			foreach ( $concatFields as $fieldName )
			{
				if ( substr( $fieldName, 0, 2 ) == '__' && $this->recordBuilder )
				{
					$recordBuilderField = substr( $fieldName, 2 );
					$values[] = $this->recordBuilder->{$recordBuilderField};
				}
				else {
					$values[] = $ds->getVar( $fieldName );
				}
			}

			$value = implode( $this->_ro_concatDelimiter, $values );
		}
		else
		{
			$value = $this->_ro_staticValue;
		}
		
		if ( $this->_ro_truncateRight !== null )
		{
			$value = substr( $value, 0, $this->_ro_truncateRight );
		}
		
		if ( $this->_ro_truncateLeft !== null )
		{
			$value = substr( $value, - $this->_ro_truncateLeft );
		}  

		if ( $this->_ro_prependValue !== null )
		{
			$value = $this->_ro_prependValue . $value;
		}
		if ( $this->_ro_appendValue !== null )
		{
			$value .= $this->_ro_appendValue;
		}
		if ( $this->_ro_formatCurrency )
		{
			$parts = explode( '.', $value );
			if ( count( $parts ) == 2 )
			{
				$value = $parts[0] . str_pad( $parts[1], 2, '0' );
			}
			elseif( count( $parts ) == 1 )
			{
				$value .= '00';
			}
		}

		$value = substr( $value, 0, $this->_ro_length );

		return $value;
	}
	//------------------------------------------------------------------------
	/**
	*
	*/
	public function validate( $data )
	{
		if ( $this->_ro_enforceLength && strlen( $data ) != $this->_ro_length )
		{
			throw new tgsfException(
				'Value provided for field: ' .
				$this->_ro_fieldName .
				' does not meet the length requirement of: ' .
				$this->_ro_length . PHP_EOL . get_dump( $data )
			);
		}

		if ( $this->_ro_enforceNumeric && is_numeric( $data ) == false )
		{
			throw new tgsfException(
				'Value provided for field: ' .
				$this->_ro_fieldName .
				' is not numeric and is required to be so.' .
				PHP_EOL . get_dump( $data )
			);
		}
	}
	//------------------------------------------------------------------------
	/**
	* Builds the field and adds it to the returnRecord
	* @param DataSource: A datasource containing the field to work with for this record
	* @param Reference:String The string to append this field to
	* @return Nothing
	*/
	public function execute( $ds, &$returnRecord )
	{
		$data = $this->_getData( $ds );
		$this->validate( $data );
		$returnRecord .= str_pad( $data, $this->_ro_length, $this->_ro_padChar, $this->_ro_paddingType );
	}

	//------------------------------------------------------------------------
	/**
	* Sets the recordBuilder reference to point to the given achRecordBuilder instance
	* @param Reference:achRecordBuilder The instance of achRecordBuilder that contains extra data fields for this field
	* @return Nothing
	*/
	public function setRecordBuilder( $recordBuilder )
	{
		$this->recordBuilder = $recordBuilder;
	}

	public function clearRecordBuilder()
	{
		$this->recordBuilder = null;
	}


}
//------------------------------------------------------------------------¬
class arbItemPadLeft extends arbItem
{
	protected $_ro_paddingType = STR_PAD_LEFT;
}
//------------------------------------------------------------------------
class arbItemPadRight extends arbItem
{
	protected $_ro_paddingType = STR_PAD_RIGHT;
}
//------------------------------------------------------------------------
class achRecordBuilder extends achRecord
{
	protected $_ro_formattedRecord = '';

	public function &setControlRecord( &$controlRecord )
	{
		$this->_controlRecord =& $controlRecord;
		return $this;
	}

	public function &getControlRecord( )
	{
		return $this->_controlRecord;
	}

	//------------------------------------------------------------------------
	/**
	* Adds a field item to the internal array
	*/
	public function &addItem( $item )
	{
		$this->_items[] =& $item;
		return $this;
	}
	public function execute( &$ds )
	{
		$this->_ro_formattedRecord = '';
		$this->dataSource =& $ds;

		if ($this->_controlRecord)
		{
			$this->_controlRecord->touch( $this );
		}

		foreach( $this->_items as $item )
		{
			$item->setRecordBuilder( $this );
			$item->execute( $this->dataSource, $this->_ro_formattedRecord );
			$item->clearRecordBuilder();
		}

		return $this->_ro_formattedRecord;
	}

}

abstract class achRecord extends tgsfBase
{
	protected $_achRecord;
	protected $_ro_recordTypeCode;
	protected $_items = array();
	protected $_controlRecord = null;
	public $dataSource = null;

	protected $_recordTypeCodeChecking = true;

	// For special record types, it may be necessary to 
	// disable the record type code checks in consume()
	public function disableRecordTypeCodeChecking()
	{
		$this->_recordTypeCodeChecking = false;
		return $this;
	}

	// Consume a line from an ach file and return a datasource
	// Optionally, add the fields to the passed in datasource
	public function &consume( $achRecord, &$ds = null )
	{
		if ($ds === null)
		{
			$this->dataSource = dsFactory::ds();
		}
		else
		{
			$this->dataSource = $ds;
		}
		$this->_achRecord = $achRecord;

		if ( $this->_recordTypeCodeChecking )
		{
			// First validate that the passed record type code matches
			$recordTypeCode = $this->_items[0]->getStaticValue();
			$recordTypeCodeLength = $this->_items[0]->getLength();
			$value = substr( $achRecord, 0, $recordTypeCodeLength );
			if ( $recordTypeCode != $value )
			{
				throw new tgsfException(
					'Attempted to consume achRecord that was not of the correct record type code.  Was looking for ' .
					$recordTypeCode . ' but found ' . $value
				);
			}
		}

		// Now call validate, possibly subclasses have additional requirements
		$this->validate();

		$cursor = 0;

		foreach ( $this->_items as $item )
		{

			$fieldLength = $item->getLength();
			$fieldName = $item->getFieldName();

			$fieldValue = substr( $achRecord, $cursor, $fieldLength );
			
			// If the record format calls for a prepended, or appended value, and it is on the record, remove it
			$prependValue = $item->getPrependValue();
			$appendValue = $item->getAppendValue();
			
			// check for prepend and remove if found
			if (substr( $fieldValue, 0, strlen( $prependValue ) ) == $prependValue )
			{
				$fieldValue = substr( $fieldValue, strlen( $prependValue ) );
			}

			// check for append and remove if found
			if (substr( $fieldValue, - strlen( $appendValue ) ) == $appendValue )
			{
				$fieldValue = substr( $fieldValue, 0, strlen( $fieldValue ) - strlen( $appendValue ) );
			}
			
			// truncateRight and truncateLeft cannot be reversed, as we don't know what got truncated
			// so we won't do anything with them.  Furthermore, if it was a field originally truncated
			// we don't want to grab this data, as it will be different than what was originally in that field
			// when we built the file.  More likey than not, the original field was also included somewhere
			// else in a non-truncated form, so we'll skip extracting this field and continue with the next.
			if ( $item->getTruncateRight() !== null || $item->getTruncateLeft() !== null )
			{
				continue;
			}			
			
			if ( $fieldName && $fieldName != '' )
			{
				$item->validate( $fieldValue );
				$this->dataSource->setVar( $fieldName, $fieldValue );
			}
			
			$concatFields = $item->getConcatFields();
			$concatDelimiter = $item->getConcatDelimiter();
			
			// If this is a concatenated field, attempt to parse it into its sub-fields
			if ( is_array( $concatFields ) && $concatDelimiter )
			{
				$fieldParts = explode( $concatDelimiter, $fieldValue );
				$fieldPartCount = 0;
				if ( count( $fieldParts ) == count ($concatFields) )
				{
					foreach ( $concatFields as $fieldName )
					{
						$this->dataSource->setVar( $fieldName, $fieldParts[$fieldPartCount] );
					}
				}
			}
			
			$cursor += $fieldLength;
		}

		if ($this->_controlRecord)
		{
			$this->_controlRecord->touch( $this );
		}

		return $this->dataSource;
	}

	public function validate()
	{
	}

}


// For building any type of detail records
class achDetailRecord extends achRecordBuilder
{
	// class extended for identification purposes
}

// For building any type of addenda records
class achAddendaRecord extends achRecordBuilder
{
	// class extended for identification purposes
}

// For building batch header records
class achBatchHeaderRecord extends achRecordBuilder
{
	// class extended for identification purposes
}

// For building file header records
class achFileHeaderRecord extends achRecordBuilder
{
	// class extended for identification purposes
}

// Abstraction for control records
abstract class achControlRecord extends achRecordBuilder
{
	abstract public function &touch( &$recordBuilder );
	abstract public function reset( );

	public function execute( &$ds )
	{
		$this->_ro_formattedRecord = '';
		$this->dataSource =& $ds;

		if ($this->_controlRecord)
		{
			$this->_controlRecord->touch( $this );
		}

		foreach( $this->_items as $item )
		{
			$item->setRecordBuilder( $this );
			$item->execute( $ds, $this->_ro_formattedRecord );
			$item->clearRecordBuilder();
		}

		$this->reset();

		return $this->_ro_formattedRecord;
	}
}

// For building batch control records - maintains totals and checksums for the batch
class achBatchControlRecord extends achControlRecord
{
	public $entryAddendaCount = 0;
	public $entryCount = 0;
	public $entryHash = 0;
	public $totalDebits = 0;
	public $totalCredits = 0;

	public function reset( )
	{
		$this->entryAddendaCount = 0;
		$this->entryCount = 0;
		$this->entryHash = 0;
		$this->totalDebits = 0;
		$this->totalCredits = 0;
	}

	// @param DataSource ach_txn data source to add to the the control record summary
	public function &touch( &$recordBuilder )
	{

		$this->dataSource =& $recordBuilder->dataSource;

		if ( $recordBuilder instanceOf achDetailRecord )
		{
			// Increment the detail/addenda count
			$this->entryAddendaCount++;

			// Increment the detail count
			$this->entryCount++;

			// The entry hash is the sum of all routing numbers (without the checksum)
			// and then truncated to 10 digits.  For expediency, we'll truncate when building
			// the control record
			// For IATs, the hash comes from the gateway operator Receiving DFI ID
			if ( $recordBuilder instanceOf iatDetailRecord )
			{
				$newEntryHash = $this->entryHash + substr($this->dataSource->ach_txn_go_receiving_dfi_id, 0, 8);
			}
			// For domestic detail records, the hash comes from the routing number
			else
			{ 
				$newEntryHash = $this->entryHash + substr($this->dataSource->ach_txn_external_account_routing, 0, 8);
			}
			$this->entryHash = substr($newEntryHash, 0, 10);

			// Add to the running total of credits or debits
			if ( $this->dataSource->ach_txn_type == attBUY )
			{
				// Total credits are stored without a decimal place as an integer
				$this->totalDebits += $this->dataSource->ach_txn_amount;
			}
			else
			{
				// Total credits are stored without a decimal place as an integer
				$this->totalCredits += $this->dataSource->ach_txn_amount;
			}
		}
		elseif ( $recordBuilder instanceOf achAddendaRecord )
		{
			// Increment the detail/addenda count
			$this->entryAddendaCount++;
		}

		if ( $this->_controlRecord )
		{
			$this->_controlRecord->touch( $recordBuilder );
		}

		return $this;

	}
}

// For building file control records - maintains totals and checksums for the file
class achFileControlRecord extends achControlRecord
{

	public $batchCount = 0;
	public $entryAddendaCount = 0;
	public $entryHash = 0;
	public $totalDebits = 0;
	public $totalCredits = 0;
	public $recordCount = 0;
	public $blockCount = 0;

	public function reset( )
	{
		$this->batchCount = 0;
		$this->entryAddendaCount = 0;
		$this->entryHash = 0;
		$this->totalDebits = 0.00;
		$this->totalCredits = 0.00;
		$this->recordCount = 0;
		$this->blockCount = 0;
	}

	// @param DataSource ach_batch data source to add to the the control record summary
	public function &touch( &$recordBuilder )
	{
		$this->dataSource = $recordBuilder->dataSource;

		if ( $this->_controlRecord )
		{
			$this->_controlRecord->touch( $recordBuilder );
		}

		// Increment the total record count (used to calculate the block count)
		$this->recordCount++;

		if ( $recordBuilder instanceOf achDetailRecord )
		{
			// Increment the detail/addenda count
			$this->entryAddendaCount++;

			// The entry hash is the sum of all routing numbers (without the checksum)
			// and then truncated to 10 digits.  For expediency, we'll truncate when building
			// the control record
			// For IATs, the hash comes from the gateway operator Receiving DFI ID
			if ( $recordBuilder instanceOf iatDetailRecord )
			{
				$newEntryHash = $this->entryHash + substr($this->dataSource->ach_txn_go_receiving_dfi_id, 0, 8);
			}
			// For domestic detail records, the hash comes from the routing number
			else
			{
				$newEntryHash = $this->entryHash + substr($this->dataSource->ach_txn_external_account_routing, 0, 8);
			}

			$this->entryHash = substr($newEntryHash, 0, 10);

			// Add to the running total of credits or debits
			if ( $this->dataSource->ach_txn_type == attBUY )
			{
				// Total credits are stored without a decimal place as an integer
				$this->totalDebits += $this->dataSource->ach_txn_amount;
			}
			else
			{
				// Total credits are stored without a decimal place as an integer
				$this->totalCredits += $this->dataSource->ach_txn_amount;
			}
		}
		elseif ( $recordBuilder instanceOf achAddendaRecord )
		{
			// Increment the detail/addenda count
			$this->entryAddendaCount++;
		}
		elseif ( $recordBuilder instanceOf achBatchHeaderRecord )
		{
			// Increment the batch count
			$this->batchCount++;
		}

		// Recalculate the block count - 1 block for every 10 records, rounded up
		$this->blockCount = ceil( $this->recordCount / 10 );

		return $this;

	}

}


// For building IAT detail records (includes additional fields pulled from config)
class iatDetailRecord extends achDetailRecord
{
	public $receivingDFIIdentification = '';
	public $addendaRecordCount = 0;
	protected $addendaRecords = array();
	
	public function reset()
	{
		$this->addendaRecords = array();
		$this->addendaRecordCount = 0;
	}

	public function &addAddenda( &$achAddenda )
	{
		if ( ! $achAddenda instanceof achAddendaRecord )
		{
			throw new tgsfException( 'prepareAddenda() was passed something other than an achAddendaRecord' );
		}

		$this->addendaRecords[] = $achAddenda;
		$this->addendaRecordCount++;
		
		return $this;
	}
	
	public function execute( &$ds )
	{
		$achData = array();
	
		// Execute this detail record
		$achData[] = parent::execute( $ds );

		// Execute all the addenda records
		foreach( $this->addendaRecords as $addendaRecord )
		{
			$achData[] = $addendaRecord->execute( $ds );
		}

		$this->reset();

		return implode( "\n", $achData );
	}
}

class iatAddendaRecord extends achAddendaRecord
{
	// Second IAT Addenda
	public $achOriginatorName = '';
	public $achOriginatorStreetAddress = '';
	
	// Third IAT Addenda
	public $achOriginatorCity = '';
	public $achOriginatorState = '';
	public $achOriginatorCountry = '';
	public $achOriginatorPostalCode = '';
	
	// Fourth IAT Addenda
	public $achOriginatingDFIName = '';
	public $achOriginatingDFIIdQualifier = '';
	public $achOriginatingDFIId = '';
	public $achOriginatingDFIBranchCountryCode = '';

	public function __construct()
	{
		// Second IAT Addenda
		$this->achOriginatorName		= config( 'ach_immediate_origin_name' );
		$this->achOriginatorStreetAddress 	= config( 'ach_iat_originator_street_address' );

		// Third IAT Addenda
		$this->achOriginatorCity 		= config( 'ach_iat_originator_city' );
		$this->achOriginatorState		= config( 'ach_iat_originator_state' );
		$this->achOriginatorCountry		= config( 'ach_iat_originator_country' );
		$this->achOriginatorPostalCode		= config( 'ach_iat_originator_postal_code' );

		// Fourth IAT Addenda
		$this->achOriginatingDFIName		= config( 'ach_iat_originating_dfi_name' );
		$this->achOriginatingDFIBranchCountryCode	= config( 'ach_iat_originating_dfi_country' );
		$this->achOriginatingDFIId		= config( 'ach_iat_originating_dfi_id' );
		$this->achOriginatingDFIIdQualifier	= config( 'ach_iat_originating_dfi_id_qualifier' );		
	}
	
}

